/**
 * Cryptor Testing harness for validating correct functioning of the
 * abstraction layer.
 */
module CryptoTest;

import Integer = tango.text.convert.Integer;

import tango.io.Stdout;
import tango.io.device.File;
import tango.io.stream.TextFile;
import tango.io.stream.Format;
import tango.text.Search;

import tango.util.cipher.Cipher;
import tango.util.cipher.Cryptor;

import tango.util.cipher.AES;
import tango.util.cipher.Serpent;
import tango.util.cipher.Blowfish;
import tango.util.cipher.RC6;
import tango.util.cipher.TEA;
import tango.util.cipher.XTEA;

import tango.util.cipher.RC4;
import tango.util.cipher.Salsa20;
import tango.util.cipher.ChaCha;

import tango.util.cipher.modes.ECB;
import tango.util.cipher.modes.CBC;
import tango.util.cipher.modes.CTR;
import tango.util.cipher.modes.CFB;
import tango.util.cipher.modes.OFB;
import tango.util.cipher.modes.PCBC;

import tango.util.cipher.pad.NoPadding;
import tango.util.cipher.pad.PKCS7;
import tango.util.cipher.pad.ANSIx923;
import tango.util.cipher.pad.ISO10126;
import tango.util.cipher.pad.ISO_IEC9797_1;

import tango.util.cipher.order.NoOrdering;
import tango.util.cipher.order.ROR;
import tango.util.cipher.order.ROL;


//key material and initialization vector to use for the testing
const string keyMaterial = "FFEEDDCCBBAA99887766554433221100AAF";
const string ivMaterial = x"0000AAAA0000BBBB0000";
string[] testTexts = ["AAAAAAAABBBBBBBBAAAAAAAABBBBBBBBAAAAAAAABBBBBBBBAAAAAAAABBB",
                     "The quick brown fox jumped over the fence."];

void main(string[] args){
	CipherParameters key = new SymmetricKey(keyMaterial);
	CipherParameters iv = new IVParameters(ivMaterial);
	bool failures = false;
	bool warnings = false;
	
	specialTests();
	
	//open log for writing
	auto log = new FormatOutput!(char)(new TextFileOutput("log.xml", File.WriteCreate));

	logEntry(log,entry.head);
		
	//blockCipherTests(testTexts, key, iv, failures, warnings, log);
	//streamCipherTests(testTexts, key, iv, failures, warnings, log);	
	
	if(failures) {
		Stderr.formatln("\nAt least one test failed!! Check the error log for more info.");
	}else if(warnings) {
		Stdout.formatln("\nAll tests passed, some warnings generated!! Check the warnings log for more info.");
	}else {
		Stdout.formatln("\nAll tests passed!! Check the data log for more info.");
	}
	
	logEntry(log,entry.tail);
}

void specialTests() {
}

void streamCipherTests(string[] texts, CipherParameters key, CipherParameters iv,
		ref bool failures, ref bool warnings, FormatOutput!(char) log) {

	Cipher[] testStreamCiphers = [cast(Cipher)new RC4(), new Salsa20(), new ChaCha()];	
	testStreamCiphers.sort;
	
	//test each combination of parameters
	foreach(Cipher cw; testStreamCiphers){
		logEntry(log,entry.cipher,cw.name);
		try {
			Cryptor c1 = new Cryptor(), c2;
			c1.init(cw, [key, iv]);
			c2 = c1.dup();
			
			foreach(string pt1; texts){
				fullTest(c1, pt1, failures, log, 2, c2);
			}
		}catch(IllegalArgumentException e){
			auto match = search("allow reordering");
			auto match2 = search("requires padding");
			if(match.within(e.msg) || match2.within(e.msg)){
				logEntry(log,entry.warning,e.msg);
				warnings = true;
			}else{
				logEntry(log,entry.error,e.msg);
				failures = true;
			}
		}catch(Exception e1){
			logEntry(log,entry.error,e1.msg);
			failures = true;
		}
		logEntry(log,entry.cipher,cw.name, false);
	}
}

void blockCipherTests(string[] texts, CipherParameters key, CipherParameters iv,
		ref bool failures, ref bool warnings, FormatOutput!(char) log) {
	
	Cipher[] testBlockCiphers = [cast(Cipher)new AES(), new Serpent(), new Blowfish(), new RC6(), new TEA(), new XTEA()];
	testBlockCiphers.sort;
	
	//cipher block modes to test
	BlockCipherMode[] testModes = 
		[cast(BlockCipherMode)new ECB(),new CBC(),new PCBC(),new CTR(),new CFB(),new OFB];
	
	//padding schemes to test
	BlockCipherPadding[] testPads = 
		[cast(BlockCipherPadding)new NoPadding(),new ANSIx923(), new PKCS7(),
		 new ISO10126(), new ISO_IEC9797_1()];
	
	//block reordering schemes to test
	BlockCipherOrdering[] testOrders = 
		[cast(BlockCipherOrdering)new NoOrdering(),new ROL(), new ROR()];	

	
	//test each combination of parameters
	foreach(Cipher cw; testBlockCiphers){
		logEntry(log,entry.cipher,cw.name);
		foreach(BlockCipherMode mode; testModes){
			logEntry(log,entry.mode,mode.name);
			foreach(BlockCipherPadding pad; testPads){
				logEntry(log,entry.pad,pad.name);
				foreach(BlockCipherOrdering inord; testOrders){
					foreach(BlockCipherOrdering outord; testOrders){
						logEntry(log,entry.order,inord.name ~ "/" ~ outord.name);
						try{
							Cryptor c1 = new Cryptor(), c2;
							c1.init(cw, [key, iv, mode.dup, pad.dup, inord.dup, outord.dup]);
							c2 = c1.dup();
							
							foreach(string pt1; texts){
								logEntry(log,entry.test,pt1);
								fullTest(c1, pt1, failures, log, 2, c2);
								logEntry(log,entry.test,pt1,false);
							}
						}catch(IllegalArgumentException e){
							auto match = search("allow reordering");
							auto match2 = search("requires padding");
							if(match.within(e.msg) || match2.within(e.msg)){
								logEntry(log,entry.warning,e.msg);
								warnings = true;
							}else{
								logEntry(log,entry.error,e.msg);
								failures = true;
							}
						}catch(Exception e1){
							logEntry(log,entry.error,e1.msg);
							failures = true;
						}
						logEntry(log,entry.order,outord.name, false);
					}
				}
				logEntry(log,entry.pad,pad.name, false);
			}
			logEntry(log,entry.mode,mode.name, false);
		}
		logEntry(log,entry.cipher,cw.name, false);
	}
}

/**
 * Full output test on a single cipher
 * 
 * Params:
 *     crypt = The cryptor to use
 *     pt1 = The string to encrypt
 */
void fullTest(Cryptor crypt, string pt1, ref bool failures, FormatOutput!(char) log, int iterations = 1, Cryptor decrypt = null){
	bool copy = (decrypt is null);
	if(copy) decrypt = crypt;
	
	for(int iter = iterations; iter>0; iter--) {
		logEntry(log,entry.set,"");
		testInternal(crypt, decrypt, pt1, failures, log, true);
		testInternal(crypt, decrypt, pt1, failures, log, false);
		testInternal(crypt, decrypt, pt1, failures, log, false);
		testInternal(crypt, decrypt, pt1, failures, log, false);
		testInternal(crypt, decrypt, pt1, failures, log, false);
		logEntry(log,entry.set,"",false);

		logEntry(log,entry.set,"");
		testInternal(decrypt, crypt, pt1, failures, log, true);
		testInternal(decrypt, crypt, pt1, failures, log, false);
		testInternal(decrypt, crypt, pt1, failures, log, false);
		testInternal(decrypt, crypt, pt1, failures, log, false);
		testInternal(decrypt, crypt, pt1, failures, log, false);
		logEntry(log,entry.set,"",false);

	}
}

void testInternal(Cryptor crypt, Cryptor decrypt, string pt1, ref bool failures, FormatOutput!(char) log, bool carry) {
	string et1, dt1; 
	
	et1 = cast(string)crypt.encrypt(pt1, carry);
	dt1 = cast(string)decrypt.decrypt(et1, carry);
	

	logEntry(log,entry.ciphertext,et1);
	logEntry(log,entry.decodedtext,dt1);
	
	if(pt1 <> dt1) {
		logEntry(log, entry.error, "Cipher Decode failure");
		failures = true;
	}
	logEntry(log,entry.ciphertext,et1,false);
}

const struct entry{
	const string head = "head";
	const string tail = "tail";
	const string cipher = "cipher";
	const string mode = "mode";
	const string pad = "pad";
	const string order = "order";
	const string set = "set";
	const string test = "test";
	const string error = "error";
	const string warning = "warning";
	const string ciphertext = "ciphertext";
	const string decodedtext = "decodedtext";
}

string indent;

void logEntry(FormatOutput!(char) file, string item, string message="", bool open = true) {
	switch (item) {
		case entry.head:{
			file.formatln("<?xml version=\"1.0\" encoding=\"UTF-16\"?>\n<instance>");
			indent = "\t";
			break;
		}
		case entry.tail:{
			file.formatln("</instance>");
			indent = "";
			break;
		}
		case entry.cipher:
		case entry.mode:
		case entry.pad:
		case entry.order:{
			if(open) {
				file.formatln("{}<{} name=\"{}\">", indent, item, message);
				indent ~= "\t";
			}else {
				indent.length = indent.length - 1;
				file.formatln("{}</{}>", indent, item);
			}
			break;
		}
		case entry.test:{
			if(open) {
				file.formatln("{0}<{1} plaintext=\"{2}\">",indent,item,message);
				indent ~= "\t";
			}else {
				indent.length = indent.length - 1;
				file.formatln("{}</{}>", indent, item);
			}
			break;
		}
		case entry.set:{
			if(open) {
				file.formatln("{0}<{1}>",indent,item);
				indent ~= "\t";
			}else {
				indent.length = indent.length - 1;
				file.formatln("{}</{}>", indent, item);
			}
			break;
		}
		case entry.ciphertext:{
			if(open) {
				file.formatln("{0}<{1} text=\"{2}\">",indent,item,message);
				indent ~= "\t";
			}else {
				indent.length = indent.length - 1;
				file.formatln("{}</{}>", indent, item);
			}
			break;
		}			
		case entry.decodedtext:{
			file.formatln("{0}<{1} text=\"{2}\" />",indent,item,message);
			break;
		}
		case entry.error:{
			file.formatln("{0}<{1}>{2}</{1}>",indent,item,message);
			break;
		}
		case entry.warning:{
			file.formatln("{0}<{1}>{2}</{1}>",indent,item,message);
			break;
		}
		default:{}
	}
	file.flush;
}

